跳到主要内容

前端工程化

备注

2024 年 11 月 2 日,听了一个大佬的分享,分享了前端工程化的知识感触很多,这里更新一下

前端工程化是什么

之前我只是了解到打包工具有个 vite 有个 webpack,vite 比 webpack 要快,可以使用什么 eslint commitlint,prettier,husky,lint-staged,stylelint,commitlint 这些东西可以让你代码更加规范,更容易读懂

前端工程化还有什么呢?

这位大佬的话是这样的

important

前端工程化的目标是通过自动化标准化最佳实践来提高效率,降低因为频繁的人工干预而导致问题出现的可能性, 以及实现对项目性能多方面的优化

我们通过自动化,标准化,最佳实践来实现前端工程化 可以解决效率,质量,性能等问题

前端工程化的例子

可以使用 CI 持续集成,比如大型的开源项目经常能看到类似的东西,使用 Github Actions 这些大型的开源项目,会在每次代码提交后,自动执行构建,检查是否存在构建过程, 自动化代码的检查,在代码提交前,自动化检查代码是否符合规范,比如 eslint,stylelint,commitlint 自动化测试,自动化测试代码是否符合预期,比如 jest,mocha,karma 自动化部署,自动化部署到服务器,比如 Vercel

了解过 AST 吗?

代码的本质是字符串,字符串的一些操作就是所谓的编译

编译器有输入和输出,输入的是原始的 Code,输出的是目标 Code Lexer 是词法分析器,将字符串转换为 TokensTokens 是一个个的词法单元 Parser 是语法分析器将 Token 转换为 AST AST 是语法树,进行语义分析,生成 analyzedAST(分析后的 AST) Code Generator 是代码生成器,将 analyzedAST 转换为目标 Code

词法分析首先将源代码转换成单词流也就是词法单元,每个词法单元都有一个标识符和一个属性值 语法分析会将词法单元流转化为抽象语法树也是就是 AST,表示源代码的结构和原则 语义分析会在 AST 上进行一些操作比如说类型检查,作用域检查,以确保代码的正确性和安全性 代码生成器会将 AST 转化为目标代码,这个目标代码可以是机器码,也可以是其他代码

一个编译器最核心的代码

function compile(input) {
let tokens = lexer(input);
let ast = parser(tokens);
let analyzedAST = semanticAnalysis(ast);
let output = codeGenerator(analyzedAST);
return output;
}

首先将输入的代码转化为词法单元,然后将词法单元转化为 AST,然后在 AST 上进行语义分析,最后将分析后的 AST 转化为目标代码

为什么在工作中要使用编译原理,一个公式编辑器,一个字符串复杂的处理,需要掌握 AST 及编译原理

将 LISP 语言转化为 JS 语言

function tokenizer(input) {
let current = 0;
let tokens = [];
while (current < input.length) {
let char = input[current];
if (char === "(") {
tokens.push({
type: "paren",
value: "(",
});
current++;
continue;
}
if (char === ")") {
tokens.push({
type: "paren",
value: ")",
});
current++;
continue;
}
let WHITESPACE = /\s/;
if (WHITESPACE.test(char)) {
current++;
continue;
}
let NUMBER = /[0-9]/;
if (NUMBER.test(char)) {
let value = "";
while (NUMBER.test(char)) {
value += char;
char = input[++current];
}
tokens.push({
type: "number",
value,
});
continue;
}
if (char === '"') {
let value = "";
char = input[++current];
while (char !== '"') {
value += char;
char = input[++current];
}
char = input[++current];
tokens.push({
type: "string",
value,
});
continue;
}
let LETTERS = /[a-z]/i;
if (LETTERS.test(char)) {
let value = "";
while (LETTERS.test(char)) {
value += char;
char = input[++current];
}
tokens.push({
type: "name",
value,
});
continue;
}
throw new TypeError("I dont know what this character is: " + char);
}
return tokens;
}

以上就是一个简单的词法分析器

//parser
function parser(tokens) {
let current = 0;
function walk() {
let token = tokens[current];
if (token.type === "number") {
current++;
return {
type: "NumberLiteral",
value: token.value,
};
}
if (token.type === "string") {
current++;
return {
type: "StringLiteral",
value: token.value,
};
}
if (token.type === "paren" && token.value === "(") {
token = tokens[++current];
let node = {
type: "CallExpression",
name: token.value,
params: [],
};
token = tokens[++current];
while (
token.type !== "paren" ||
(token.type === "paren" && token.value !== ")")
) {
node.params.push(walk());
token = tokens[current];
}
current++;
return node;
}
throw new TypeError(token.type);
}
let ast = {
type: "Program",
body: [],
};
while (current < tokens.length) {
ast.body.push(walk());
}
return ast;
}

以上就是一个简单的语法分析器

//traverser
function traverser(ast, visitor) {
function traverseArray(array, parent) {
array.forEach((child) => {
traverseNode(child, parent);
});
}
function traverseNode(node, parent) {
let method = visitor[node.type];
if (method && method.enter) {
method.enter(node, parent);
}
switch (node.type) {
case "Program":
traverseArray(node.body, node);
break;
case "CallExpression":
traverseArray(node.params, node);
break;
case "NumberLiteral":
case "StringLiteral":
break;
default:
throw new TypeError(node.type);
}
if (method && method.exit) {
method.exit(node, parent);
}
}
traverseNode(ast, null);
}

以上就是一个简单的遍历器

//transformer
function transformer(ast) {
let newAst = {
type: "Program",
body: [],
};
ast._context = newAst.body;
traverser(ast, {
NumberLiteral: {
enter(node, parent) {
parent._context.push({
type: "NumberLiteral",
value: node.value,
});
},
},
StringLiteral: {
enter(node, parent) {
parent._context.push({
type: "StringLiteral",
value: node.value,
});
},
},
CallExpression: {
enter(node, parent) {
let expression = {
type: "CallExpression",
callee: {
type: "Identifier",
name: node.name,
},
arguments: [],
};
node._context = expression.arguments;
if (parent.type !== "CallExpression") {
expression = {
type: "ExpressionStatement",
expression: expression,
};
}
parent._context.push(expression);
},
},
});
return newAst;
}

以上就是一个简单的转换器

//Code Generator
function codeGenerator(node) {
switch (node.type) {
case "Program":
return node.body.map(codeGenerator).join("\n");
case "CallExpression":
return (
codeGenerator(node.callee) +
"(" +
node.params.map(codeGenerator).join(", ") +
")"
);
case "NumberLiteral":
return node.value;
case "StringLiteral":
return '"' + node.value + '"';
default:
throw new TypeError(node.type);
}
}

babel 的 plugin 和 loader 的应用与原理

babel 是一个 JavaScript 编译器,可以将 ES6 代码转换为 ES5 代码,也可以将 JSX 转换为 JS @babel/core @babel/preset-env @babel/parser @babel/traverse @babel/generator babel 和前面介绍的编译原理是一样的,只不过 babel 是一个工具,可以将代码转化为目标代码

input source code -> babel parser -> AST -> babel traverse -> AST -> babel generator -> output code

import %%importName%% from '%%importPath%%';
import module from './module';

import { template } from "@babel/core";
import * as t from "@babel/types";
import generate from "@babel/generator";

const buildRequire = template(`
var %%IMPORT_NAME%% = require(%%SOURCE%%);
`);

const ast = buildRequire({
IMPORT_NAME: t.identifier("myModule"),
SOURCE: t.stringLiteral("my-module"),
});
console.log(generate(ast).code);
// var myModule = require("my-module");

以上是一个简单的 babel 插件,主要是将 import 转化为 require

babel 除了转译代码,还可以做一些其他的事情 pollyfill,tree shaking,代码压缩等

说说 webpack 的打包过程和原理

  1. SplitChunk
  2. Tree Shaking
  3. Dll
  4. Css 提取
  5. Terser
  6. mode

webpack 构建流程 几个核心的概念

  1. Compiler
  2. Compilation
  3. Module
  4. Chunk
  5. Bundle
  6. Loader

执行过程

  1. 初始化,初始化会读取配置信息,统计入口文件,解析 loader 和 plugin
  2. 编译阶段,webpack 编译代码,部分依赖 babel,ts 转为 JavaScript,less 转为 css
  3. 输出阶段,生成输出文件包括文件名,输出路径,资源信息等

初始化阶段的主要流程

  1. 初始化参数,从配置文件和 shell 语句中读取与合并参数,得出最终的参数
  2. 创建 Compiler 对象实例,加载所有配置的插件,插件是 webpack 的核心,webpack 本身也是基于插件运行的
  3. 开始编译,调用 Compiler 对象的 run 方法开始执行编译
  4. 确定入口,通过 entry 配置找到所有的入口文件,调用 addEntry 方法添加入口文件,当 addEntry 被调用时,会触发钩子

构建阶段

  1. 编译模块,通过 entry 对应的 dependence 创建 module 对象,(先有 module 再有 chunk,然后有 bundle),调用对应 loader 对模块进行编译,将模块转为 js 内容,babel 将一些内容转化为目标的代码内容
  2. 完成模块的编译最后得到一个 moduleGraph

生成阶段

  1. 输出资源 组装 chunk,chunkGroup,再将 Chunk 转换为一个单独的文件加入到输出列表,说明这里是修改资源内容的最后机会
  2. 写入文件系统,将文件写入到文件系统,确定好输出内容后,根据配置输出到文件中

loader 的本质是对象 plugin 的本质是函数